/**
 *
 */
package org.jprobe.junit;

import java.lang.reflect.Modifier;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.jprobe.JavaProjectInterface;
import org.jprobe.classpath.ClassPathInterface;
import org.jprobe.classpath.ClassPathReader;
import org.jprobe.instantiate.Instantiator;
import org.jprobe.instantiate.InstantiatorException;


/**
 * this object can be used to generate
 * junit test cases on class,package, or
 * project level
 *
 * @author Hussain Al-Mutawa
 * @version 2.0
 * @since 2.0
 */
public abstract class AbstractJUnitGenerator extends AbstractTimeRegister implements Runnable,JunitGeneratorInterface {
	/*  */
	@Override
	public abstract int getTimeOut();
	@Override
	public abstract void progressChanged(int progress);
	/** the project associated with this test cases **/
	private transient final JavaProjectInterface project;
	/** the instantiator associated with this generator **/
	private transient final Instantiator instantiator;
	/** generated classes (successful test cases) **/
	private transient final Map<String,Set<String>> testcasesReport;
	/** the current progress **/
	private volatile int currentProgress=0;
	/** the total number of transactions **/
	private transient int totalTransactions=0;

	private transient String currentItem;

	public String getCurrentItem() {
		return this.currentItem;
	}

	/**
	 * intantiates the objecvt and load the project in the classpath if it has not been
	 * loaded yet
	 * @param javaProject java project for which test cases are generated
	 */
	public AbstractJUnitGenerator(final JavaProjectInterface javaProject) {
		super();
		this.currentItem="none";
		try{
			if(!javaProject.isLoaded()){javaProject.load();}
		}catch(Exception exp){throw new JUnitTestGenerationException(exp);}

		this.project=javaProject;
		this.instantiator=new Instantiator();
		this.testcasesReport=new HashMap<String,Set<String>>();
		final String[] categories = {
				"successful","TimeoutException",
				"InterruptedException","ExecutionException",
				"NullPointerException","InstantiatorException"
		};
		for(final String category:categories){
			this.testcasesReport.put(category, new HashSet<String>());
		}
	}
	/**
	 * gets the current progress of generating the test cases
	 * @return the current progress
	 */
	public synchronized int getCurrentProgress() {
		return currentProgress;
	}
	/**
	 * gets the total number of transactions
	 * @return number of transactions
	 */
	public int getTotalTransactions() {
		return totalTransactions;
	}

	/*  */
	@Override
	public Object[] getTestCasesThrowingException(final String reportCategory){
		return testcasesReport.get(reportCategory).toArray();
	}
	/*  */
	@Override
	public Object[] getReportCategories(){
		return this.testcasesReport.keySet().toArray();
	}

	/*  */
	@Override
	public double getSuccessRate(){
		final int successful = this.testcasesReport.get("successful").size();
		int total=0;
		for(Set<String>set:this.testcasesReport.values()){
			total+=set.size();
		}
		return successful*1.0d/total;
	}
	/*  */
	@Override
	public void generateAll(){

		out("+STARTED TESTING PROJECT ["+project.getName()+"]");
		out(" packages found : " + project.getPackages().length);
		this.currentProgress=0;
		this.totalTransactions=0;
		final ClassPathInterface CP = ClassPathReader.instance.getClassPath();
		for(final String pkg:project.getPackages()){
			this.totalTransactions+=CP.getPackage(pkg).getClassesCount();
		}
		startTimeCount();
		for(final String pkg:project.getPackages()){
			generateForPackage(pkg);
		}
		out("FINISHED TESTING PROJECT ["+project.getName()+"]");
		out("\n\n\n\n");
		printReport();
		this.currentProgress=this.totalTransactions;
		this.progressChanged(currentProgress);
		this.totalTransactions=0;
		stopTimeCount();
		this.currentItem="none";
	}
	/*  */
	@Override
	public void printReport() {
		out("--------------------------");
		for(String str:testcasesReport.keySet()){
			out( str +'\t'+ testcasesReport.get(str).size());
		}
		out("--------------------------");
		out("SUCCESS RATE : "+ NumberFormat.getPercentInstance().format(getSuccessRate()));
		out("--------------------------");
	}
	/*  */
	@Override
	public void generateTestCasesForPackage(final String pkg){

		totalTransactions=0;
		this.currentProgress=0;
		final ClassPathInterface CP = ClassPathReader.instance.getClassPath();

		this.totalTransactions+=CP.getPackage(pkg).getClassesCount();
		startTimeCount();
		generateForPackage(pkg);
		totalTransactions=0;
		stopTimeCount();
		this.currentItem="none";
	}

	private void generateForPackage(String pkg) {

		Class<?>[] classes;
		try {
			classes = ClassPathReader.instance.getClassPath().getClasses(pkg);
		} catch (ClassNotFoundException cnfe) {
			throw new JUnitTestGenerationException(cnfe);
		}
		out("started testing package [ "+ pkg +"]");
		out(" classes found : " + classes.length);
		for(final Class<?> clazz:classes){
			if(!(clazz==null || Modifier.isPrivate(clazz.getModifiers()) || clazz.getName().contains("JProbe") || clazz.getName().contains("Test"))){
				generateTestCasesForClass(clazz);
			}
		}
		out("FINISHED testing package [ "+ pkg +"]");

	}
	/*  */
	@Override
	public void generateTestCasesForClass(final Class<?>clazz){
		this.currentItem=clazz.getName();
		final ExecutorService executor = Executors.newCachedThreadPool();
		final Callable<Object> task = new Callable<Object>() {
			public Object call() throws Exception{
				if(instantiator.getInitializedObject(clazz)!=null){
					new JUnitTestCaseGenerator(project,clazz)
					.generate()
					.generateTestCase();
				}
				addToReport("successful",clazz.getName());
				return null;
			}
		};
		final Future<Object> future = executor.submit(task);
		try {
			future.get(getTimeOut(), TimeUnit.SECONDS);
		} catch (TimeoutException exp) {
			// handle the timeout
			addToReport("TimeoutException",clazz.getName());
			err("?[ <"+ clazz +"> timed out ] " + exp.getMessage());
			future.cancel(true);
		} catch (InterruptedException exp) {
			addToReport("InterruptedException",clazz.getName());
			err("[ <"+ clazz +"> failed ] " + exp.getMessage());
		} catch (ExecutionException exp) {
//			exp.printStackTrace();
//			System.exit(-1);
			addToReport("ExecutionException",clazz.getName());
			err("[ <"+ clazz +"> failed ] " + exp.getMessage());
		}catch(NullPointerException exp){
			addToReport("NullPointerException",clazz.getName());
			err("[ <"+ clazz +"> failed ] " + exp.getMessage());
		}catch(InstantiatorException exp){
			addToReport("InstantiatorException",clazz.getName());
			err("[ <"+ clazz +"> failed ] " + exp.getMessage());
		}catch(JUnitTestGenerationException exp){
			addToReport("JUnitTestGenerationException",clazz.getName());
			err("[ <"+ clazz +"> failed ] " + exp.getMessage());
		} finally {
			out("[ "+ clazz +" finished ]");
		}
		synchronized (AbstractJUnitGenerator.this) {
			progressChanged(++AbstractJUnitGenerator.this.currentProgress);
		}
	}
	/**
	 * adds a class to the report under the given category
	 * @param category the category under which the class is added
	 * @param className the name of the class
	 */
	protected void addToReport(final String category, final String className) {
		this.testcasesReport.get(category).add(className);
	}

	@Override
	public void run() {
		generateAll();
	}
}
